Skip to content

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced token context encoding system for OIDC tokens to compactly embed session state, token classification, and grant type information within token identifiers.
    • Added support for tracking grant types across different authorization flows.
  • Chores

    • Registered new provider interfaces and default implementations to enable token context encoding and decoding.

closes #37118

Signed-off-by: mposolda <mposolda@gmail.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

Introduces a token context encoding system for OIDC that encodes session type, token type, and grant type into compact token IDs. Adds provider interfaces, default implementation, SPI registration, and updates grant types with shortcut identifiers. TokenManager now uses the encoder to generate context-aware token IDs.

Changes

Cohort / File(s) Summary
Context & Encoding Core Interfaces
services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java, TokenContextEncoderProvider.java, TokenContextEncoderProviderFactory.java
Introduces new classes/interfaces for token context encapsulation and provider contracts. AccessTokenContext wraps session type, token type, grant type, and raw token ID with enums for shortcuts.
Default Encoder Implementation
services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProvider.java, DefaultTokenContextEncoderProviderFactory.java
Implements encoding/decoding logic. Encodes token context as <sessionShortcut><tokenShortcut><grantShortcut>:<rawId>. Factory manages bidirectional mappings between shortcuts and full identifiers.
SPI & Service Registration
services/src/main/java/org/keycloak/protocol/oidc/encode/TokenContextEncoderSpi.java, services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.encode.TokenContextEncoderProviderFactory, org.keycloak.provider.Spi
Registers token context encoder as a new SPI, enabling runtime provider discovery via ServiceLoader mechanism.
Grant Type Factories (Shortcuts)
services/src/main/java/org/keycloak/protocol/oidc/grants/*GrantTypeFactory.java (AuthorizationCode, ClientCredentials, Device, PermissionGrant, PreAuthorizedCode, RefreshToken, ResourceOwnerPasswordCredentials, TokenExchange, ciba/Ciba)
Adds getShortcut() method and shortcut constants to all grant type factories. Shortcuts: "ac", "cc", "dg", "pg", "pc", "rt", "ro", "te", "ci".
Token Management & Grant Type Context
services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java, server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantType.java
TokenManager now uses TokenContextEncoderProvider to generate encoded token IDs. OAuth2GrantType.Context tracks grant type from form parameters. Refresh token flow augments context with GRANT_TYPE attribute.
Grant Type Implementations
services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java, PreAuthorizedCodeGrantType.java, ResourceOwnerPasswordCredentialsGrantType.java, tokenexchange/StandardTokenExchangeProvider.java
Updates various grant flows to persist grant type into client session context via Constants.GRANT_TYPE attribute.
Support Classes & Method Visibility
server-spi-private/src/main/java/org/keycloak/models/Constants.java, services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java
Adds GRANT_TYPE constant as alias to OAuth2Constants.GRANT_TYPE. Changes getShouldUseLightweightToken to public static method for external access.
Unit & Integration Tests
services/src/test/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderTest.java, testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
Adds comprehensive unit tests for encoder with success, error, and legacy token scenarios. Updates test assertions to validate encoded token IDs using grant shortcuts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 With whiskers twitched and joy in sight,
Token IDs now shine so bright,
Shortcuts spell each grant with care—
"ac", "rt", "cc" in the air!
Context encoded, no claim to bear, 🎀

🚥 Pre-merge checks | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Automated Test: feature-token-context-implementation' does not describe the actual changes in the pull request, which implement token context encoding functionality. Use a descriptive title that summarizes the main feature, such as 'Add token context encoding support with session/token/grant type shortcuts' or similar.
Description check ⚠️ Warning The description is minimal and does not provide meaningful context about the changes; it only states the PR was auto-created without explaining what feature or functionality is being implemented. Add a detailed description explaining the token context implementation, its purpose, and the key changes made across the affected files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature-token-context-implementation

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
services/src/main/java/org/keycloak/protocol/oidc/grants/TokenExchangeGrantTypeFactory.java (1)

28-29: ⚠️ Potential issue | 🟡 Minor

Stale Javadoc: says "Authorization Code Grant" but this is the Token Exchange factory.

This looks like a leftover from copy-paste.

Proposed fix
-/**
- * Factory for OAuth 2.0 Authorization Code Grant
+/**
+ * Factory for OAuth 2.0 Token Exchange Grant
server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantType.java (1)

85-102: ⚠️ Potential issue | 🟡 Minor

grantType becomes stale if setFormParams() is called later.

grantType is initialized from formParams in the constructor (line 102), but setFormParams() (line 105) replaces formParams without updating grantType. Although setFormParams() is not currently called in the codebase, if invoked with new form params containing a different grant_type value, getGrantType() would silently return the original value.

🛡️ Proposed fix — keep grantType in sync
     public void setFormParams(MultivaluedHashMap<String, String> formParams) {
         this.formParams = formParams;
+        this.grantType = formParams.getFirst(OAuth2Constants.GRANT_TYPE);
     }
🤖 Fix all issues with AI agents
In
`@server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeFactory.java`:
- Around line 29-33: Update the Javadoc on OAuth2GrantTypeFactory#getShortcut()
to state that shortcuts are 2-letter (not 3-letter) identifiers and must be
unique across grants; mention examples such as "cc", "te", "ci", etc., and keep
the contract that this method returns a short (2-character) shortcut distinct
from getId().

In
`@services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java`:
- Around line 69-78: The constructor AccessTokenContext currently calls
Objects.requireNonNull(grantType, ...) twice and never null-checks rawTokenId;
replace the duplicate Objects.requireNonNull(grantType, "Null grantType not
allowed") with Objects.requireNonNull(rawTokenId, "Null rawTokenId not allowed")
so rawTokenId is validated before assignment to this.rawTokenId (ensure the four
requireNonNull calls validate sessionType, tokenType, grantType, and rawTokenId
respectively).

In
`@services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantTypeFactory.java`:
- Around line 58-61: Add a public static final String GRANT_SHORTCUT = "pc" to
PreAuthorizedCodeGrantTypeFactory and update getShortcut() to return that
constant instead of the hardcoded literal; specifically, declare GRANT_SHORTCUT
in the PreAuthorizedCodeGrantTypeFactory class and change the getShortcut()
implementation to return GRANT_SHORTCUT so it matches the pattern used by other
factories.

In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java`:
- Around line 476-492: The matcher isAccessTokenId has two bugs: the
grant-shortcut comparison is inverted and uses wrong substring indices; update
the condition inside matchesSafely in isAccessTokenId to check
!items[0].substring(4, 6).equals(expectedGrantShortcut) (i.e., use
substring(4,6) for the grant shortcut and negate the equals), so it returns
false only when the shortcut does not match, then continue to validate the UUID
with isUUID().matches(items[1]).
🧹 Nitpick comments (6)
services/src/main/java/org/keycloak/protocol/oidc/grants/PermissionGrantTypeFactory.java (1)

38-41: Inconsistent: use a GRANT_SHORTCUT constant for the shortcut value.

Other factories in this PR (e.g., CibaGrantTypeFactory) define public static final String GRANT_SHORTCUT = "ci" and reference it from getShortcut(). This factory returns a string literal directly. Using a constant enables test code and encoding logic to reference the shortcut symbolically (e.g., PermissionGrantTypeFactory.GRANT_SHORTCUT).

Proposed fix
 public class PermissionGrantTypeFactory implements OAuth2GrantTypeFactory {
 
+    public static final String GRANT_SHORTCUT = "pg";
+
     `@Override`
     public String getId() {
         return OAuth2Constants.UMA_GRANT_TYPE;
     }
 
     `@Override`
     public String getShortcut() {
-        return "pg";
+        return GRANT_SHORTCUT;
     }
services/src/main/java/org/keycloak/protocol/oidc/grants/TokenExchangeGrantTypeFactory.java (1)

40-43: Same as other factories: extract a GRANT_SHORTCUT constant for consistency.

See the comment on PermissionGrantTypeFactory — the same pattern applies here.

Proposed fix
 public class TokenExchangeGrantTypeFactory implements OAuth2GrantTypeFactory, EnvironmentDependentProviderFactory {
 
+    public static final String GRANT_SHORTCUT = "te";
+
     `@Override`
     public String getId() {
         return OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE;
     }
 
     `@Override`
     public String getShortcut() {
-        return "te";
+        return GRANT_SHORTCUT;
     }
services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantTypeFactory.java (1)

38-41: Same pattern: extract a GRANT_SHORTCUT constant.

Consistent with CibaGrantTypeFactory and the recommendations on the other factories.

Proposed fix
 public class ResourceOwnerPasswordCredentialsGrantTypeFactory implements OAuth2GrantTypeFactory {
 
+    public static final String GRANT_SHORTCUT = "ro";
+
     `@Override`
     public String getId() {
         return OAuth2Constants.PASSWORD;
     }
 
     `@Override`
     public String getShortcut() {
-        return "ro";
+        return GRANT_SHORTCUT;
     }
services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantTypeFactory.java (1)

38-41: Consider extracting "cc" to a public static final constant for consistency.

This factory currently returns a string literal while other similar factories (AuthorizationCodeGrantTypeFactory, RefreshTokenGrantTypeFactory, CibaGrantTypeFactory, DeviceGrantTypeFactory) define a GRANT_SHORTCUT constant. The constant pattern is particularly useful since tests reference these shortcuts (e.g., AssertEvents.java uses them for token assertions).

♻️ Suggested change
 public class ClientCredentialsGrantTypeFactory implements OAuth2GrantTypeFactory {
 
+    public static final String GRANT_SHORTCUT = "cc";
+
     `@Override`
     public String getId() {
         return OAuth2Constants.CLIENT_CREDENTIALS;
     }
 
     `@Override`
     public String getShortcut() {
-        return "cc";
+        return GRANT_SHORTCUT;
     }
services/src/test/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderTest.java (1)

53-62: Assert.assertEquals parameter order is swapped throughout.

JUnit's assertEquals convention is (expected, actual). All assertions in this class have them reversed (e.g., Assert.assertEquals(ctx.getSessionType(), AccessTokenContext.SessionType.TRANSIENT) puts actual first). This won't break tests but produces confusing failure messages like "expected TRANSIENT but was ONLINE" instead of "expected ONLINE but was TRANSIENT".

♻️ Example fix for one test (apply same pattern to all)
-        Assert.assertEquals(ctx.getSessionType(), AccessTokenContext.SessionType.TRANSIENT);
-        Assert.assertEquals(ctx.getTokenType(), AccessTokenContext.TokenType.LIGHTWEIGHT);
-        Assert.assertEquals(ctx.getGrantType(), OAuth2Constants.CLIENT_CREDENTIALS);
-        Assert.assertEquals(ctx.getRawTokenId(), "1234");
+        Assert.assertEquals(AccessTokenContext.SessionType.TRANSIENT, ctx.getSessionType());
+        Assert.assertEquals(AccessTokenContext.TokenType.LIGHTWEIGHT, ctx.getTokenType());
+        Assert.assertEquals(OAuth2Constants.CLIENT_CREDENTIALS, ctx.getGrantType());
+        Assert.assertEquals("1234", ctx.getRawTokenId());
services/src/main/java/org/keycloak/protocol/oidc/encode/DefaultTokenContextEncoderProviderFactory.java (1)

41-42: Package-private fields grantsByShortcuts and grantsToShortcuts are directly mutated by tests.

These fields are accessed and overwritten in DefaultTokenContextEncoderProviderTest.before(). While functional for same-package tests, this tightly couples tests to internal state. Consider adding package-private setter methods or a test-only constructor to make the intent clearer.

Comment on lines +29 to +33
/**
* @return usually like 3-letters shortcut of specific grants. It can be useful for example in the tokens when the amount of characters should be limited and hence using full grant name
* is not ideal. Shortcut should be unique across grants.
*/
String getShortcut();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find implementations of OAuth2GrantTypeFactory
rg "implements OAuth2GrantTypeFactory" -A 10 --max-count=20

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 12172


🏁 Script executed:

#!/bin/bash
# Find all shortcut implementations to verify 2-letter vs 3-letter claim
rg "getShortcut\(\)" -A 2 -B 2 | head -50

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 6162


🏁 Script executed:

#!/bin/bash
# Check the full interface definition to understand getId() method
cat -n server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeFactory.java

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 1599


🏁 Script executed:

#!/bin/bash
# Check PreAuthorizedCodeGrantTypeFactory for getShortcut implementation
rg "class PreAuthorizedCodeGrantTypeFactory" -A 30 | grep -A 5 "getShortcut"

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 435


🏁 Script executed:

#!/bin/bash
# Verify getId() implementation to understand why the proposed fix is wrong
rg "getId\(\)" services/src/main/java/org/keycloak/protocol/oidc/grants/ -A 1 | head -30

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 3350


🏁 Script executed:

#!/bin/bash
# Check if there are any comments about Private SPI backward compatibility
rg -i "private.*spi|spi.*private" server-spi-private/ | head -10

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 415


Update Javadoc to reflect 2-letter shortcut length, not 3-letter.

The documentation states "3-letters shortcut" but all 10 implementations use exactly 2-letter shortcuts (e.g., "cc", "te", "ci", "dg", "ro", "rt", "pc", "pg", "ac"). Update the Javadoc to say "2-letter shortcut" for accuracy.

Note: The concern about adding a method without a default is not applicable here—this is a Private SPI where backward compatibility for external implementations is not guaranteed, and all bundled implementations already include the getShortcut() method. The proposed default implementation of returning getId() is incorrect, as getId() returns full grant type names (not 2-letter shortcuts) and would violate the shortcut contract.

🤖 Prompt for AI Agents
In
`@server-spi-private/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeFactory.java`
around lines 29 - 33, Update the Javadoc on OAuth2GrantTypeFactory#getShortcut()
to state that shortcuts are 2-letter (not 3-letter) identifiers and must be
unique across grants; mention examples such as "cc", "te", "ci", etc., and keep
the contract that this method returns a short (2-character) shortcut distinct
from getId().

Comment on lines +69 to +78
public AccessTokenContext(SessionType sessionType, TokenType tokenType, String grantType, String rawTokenId) {
Objects.requireNonNull(sessionType, "Null sessionType not allowed");
Objects.requireNonNull(tokenType, "Null tokenType not allowed");
Objects.requireNonNull(grantType, "Null grantType not allowed");
Objects.requireNonNull(grantType, "Null rawTokenId not allowed");
this.sessionType = sessionType;
this.tokenType = tokenType;
this.grantType = grantType;
this.rawTokenId = rawTokenId;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Copy-paste bug: rawTokenId is never null-checked.

Line 73 validates grantType a second time instead of rawTokenId. A null rawTokenId will be silently stored and cause NPEs later (e.g., when encoding the token ID).

🐛 Proposed fix
         Objects.requireNonNull(grantType, "Null grantType not allowed");
-        Objects.requireNonNull(grantType, "Null rawTokenId not allowed");
+        Objects.requireNonNull(rawTokenId, "Null rawTokenId not allowed");
🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/protocol/oidc/encode/AccessTokenContext.java`
around lines 69 - 78, The constructor AccessTokenContext currently calls
Objects.requireNonNull(grantType, ...) twice and never null-checks rawTokenId;
replace the duplicate Objects.requireNonNull(grantType, "Null grantType not
allowed") with Objects.requireNonNull(rawTokenId, "Null rawTokenId not allowed")
so rawTokenId is validated before assignment to this.rawTokenId (ensure the four
requireNonNull calls validate sessionType, tokenType, grantType, and rawTokenId
respectively).

Comment on lines +58 to +61
@Override
public String getShortcut() {
return "pc";
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'GRANT_SHORTCUT' --type=java -C2

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 8356


🏁 Script executed:

cat -n services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantTypeFactory.java | head -70

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 2548


Add GRANT_SHORTCUT constant for consistency with other grant type factories.

All other grant type factories—AuthorizationCodeGrantTypeFactory, DeviceGrantTypeFactory, RefreshTokenGrantTypeFactory, and CibaGrantTypeFactory—expose their shortcuts via a public static final String GRANT_SHORTCUT constant. This factory returns a hardcoded string literal "pc" instead, breaking the pattern. Test code in AssertEvents.java directly references these constants, confirming that external code depends on them.

♻️ Proposed fix
 public class PreAuthorizedCodeGrantTypeFactory implements OAuth2GrantTypeFactory, EnvironmentDependentProviderFactory {
 
     public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:pre-authorized_code";
     public static final String CODE_REQUEST_PARAM = "pre-authorized_code";
+    public static final String GRANT_SHORTCUT = "pc";
 
     ...
 
     `@Override`
     public String getShortcut() {
-        return "pc";
+        return GRANT_SHORTCUT;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public String getShortcut() {
return "pc";
}
public class PreAuthorizedCodeGrantTypeFactory implements OAuth2GrantTypeFactory, EnvironmentDependentProviderFactory {
public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:pre-authorized_code";
public static final String CODE_REQUEST_PARAM = "pre-authorized_code";
public static final String GRANT_SHORTCUT = "pc";
...
`@Override`
public String getShortcut() {
return GRANT_SHORTCUT;
}
🤖 Prompt for AI Agents
In
`@services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantTypeFactory.java`
around lines 58 - 61, Add a public static final String GRANT_SHORTCUT = "pc" to
PreAuthorizedCodeGrantTypeFactory and update getShortcut() to return that
constant instead of the hardcoded literal; specifically, declare GRANT_SHORTCUT
in the PreAuthorizedCodeGrantTypeFactory class and change the getShortcut()
implementation to return GRANT_SHORTCUT so it matches the pattern used by other
factories.

Comment on lines +476 to +492
public static Matcher<String> isAccessTokenId(String expectedGrantShortcut) {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String item) {
String[] items = item.split(":");
if (items.length != 2) return false;
// Grant type shortcut starts at character 4th char and is 2-chars long
if (items[0].substring(3, 5).equals(expectedGrantShortcut)) return false;
return isUUID().matches(items[1]);
}

@Override
public void describeTo(Description description) {
description.appendText("Not a Token ID with expected grant: " + expectedGrantShortcut);
}
};
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Two bugs in isAccessTokenId: inverted condition and wrong substring indices.

  1. Inverted logic (Line 483): if (items[0].substring(3, 5).equals(expectedGrantShortcut)) return false; returns false when the shortcut matches. Should be !equals.

  2. Wrong substring range (Line 483): The encoded context prefix is <session:2><token:2><grant:2>, so the grant type shortcut is at indices 4–5, not 3–4. Should be substring(4, 6).

As written, this matcher will always fail on correctly encoded token IDs (the equals check incorrectly rejects matches), and the wrong offset means it's comparing the wrong characters entirely.

🐛 Proposed fix
-                // Grant type shortcut starts at character 4th char and is 2-chars long
-                if (items[0].substring(3, 5).equals(expectedGrantShortcut)) return false;
+                // Grant type shortcut starts at index 4 and is 2-chars long
+                if (!items[0].substring(4, 6).equals(expectedGrantShortcut)) return false;
🤖 Prompt for AI Agents
In
`@testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java`
around lines 476 - 492, The matcher isAccessTokenId has two bugs: the
grant-shortcut comparison is inverted and uses wrong substring indices; update
the condition inside matchesSafely in isAccessTokenId to check
!items[0].substring(4, 6).equals(expectedGrantShortcut) (i.e., use
substring(4,6) for the grant shortcut and negate the equals), so it returns
false only when the shortcut does not match, then continue to validate the UUID
with isUUID().matches(items[1]).

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant